import { parseDn } from "builder-util-runtime"
import { execFile, execFileSync, ExecFileOptions } from "child_process"
import * as os from "os"
import { Logger } from "./types"
import * as path from "path"

function preparePowerShellExec(command: string, timeout?: number) {
  // https://github.com/electron-userland/electron-builder/issues/2421
  // https://github.com/electron-userland/electron-builder/issues/2535
  // Resetting PSModulePath is necessary https://github.com/electron-userland/electron-builder/issues/7127
  // semicolon wont terminate the set command and run chcp thus leading to verification errors on certificats with special chars like german umlauts, so rather
  //   join commands using & https://github.com/electron-userland/electron-builder/issues/8162
  const executable = `set "PSModulePath=" & chcp 65001 >NUL & powershell.exe`
  const args = ["-NoProfile", "-NonInteractive", "-InputFormat", "None", "-Command", command]
  const options: ExecFileOptions = {
    shell: true,
    timeout,
  }
  return [executable, args, options] as const
}

// $certificateInfo = (Get-AuthenticodeSignature 'xxx\yyy.exe'
// | where {$_.Status.Equals([System.Management.Automation.SignatureStatus]::Valid) -and $_.SignerCertificate.Subject.Contains("CN=siemens.com")})
// | Out-String ; if ($certificateInfo) { exit 0 } else { exit 1 }
export function verifySignature(publisherNames: Array<string>, unescapedTempUpdateFile: string, logger: Logger): Promise<string | null> {
  return new Promise<string | null>((resolve, reject) => {
    // Escape quotes and backticks in filenames to prevent user from breaking the
    // arguments and perform a remote command injection.
    //
    // Consider example powershell command:
    // ```powershell
    // Get-AuthenticodeSignature 'C:\\path\\my-bad-';calc;'filename.exe'
    // ```
    // The above would work expected and find the file name, however, it will also execute `;calc;`
    // command and start the calculator app.
    //
    // From Powershell quoting rules:
    // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7
    // * Double quotes `"` are treated literally within single-quoted strings;
    // * Single quotes can be escaped by doubling them: 'don''t' -> don't;
    //
    // Also note that at this point the file has already been written to the disk, thus we are
    // guaranteed that the path will not contain any illegal characters like <>:"/\|?*
    // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
    const tempUpdateFile = unescapedTempUpdateFile.replace(/'/g, "''")
    logger.info(`Verifying signature ${tempUpdateFile}`)

    execFile(...preparePowerShellExec(`"Get-AuthenticodeSignature -LiteralPath '${tempUpdateFile}' | ConvertTo-Json -Compress"`, 20 * 1000), (error, stdout, stderr) => {
      try {
        if (error != null || stderr) {
          handleError(logger, error, stderr, reject)
          resolve(null)
          return
        }
        const data = parseOut(stdout)
        if (data.Status === 0) {
          try {
            const normlaizedUpdateFilePath = path.normalize(data.Path)
            const normalizedTempUpdateFile = path.normalize(unescapedTempUpdateFile)
            logger.info(`LiteralPath: ${normlaizedUpdateFilePath}. Update Path: ${normalizedTempUpdateFile}`)
            if (normlaizedUpdateFilePath !== normalizedTempUpdateFile) {
              handleError(logger, new Error(`LiteralPath of ${normlaizedUpdateFilePath} is different than ${normalizedTempUpdateFile}`), stderr, reject)
              resolve(null)
              return
            }
          } catch (error: any) {
            logger.warn(`Unable to verify LiteralPath of update asset due to missing data.Path. Skipping this step of validation. Message: ${error.message ?? error.stack}`)
          }
          const subject = parseDn(data.SignerCertificate.Subject)
          let match = false
          for (const name of publisherNames) {
            const dn = parseDn(name)
            if (dn.size) {
              // if we have a full DN, compare all values
              const allKeys = Array.from(dn.keys())
              match = allKeys.every(key => {
                return dn.get(key) === subject.get(key)
              })
            } else if (name === subject.get("CN")!) {
              logger.warn(`Signature validated using only CN ${name}. Please add your full Distinguished Name (DN) to publisherNames configuration`)
              match = true
            }
            if (match) {
              resolve(null)
              return
            }
          }
        }

        const result = `publisherNames: ${publisherNames.join(" | ")}, raw info: ` + JSON.stringify(data, (name, value) => (name === "RawData" ? undefined : value), 2)
        logger.warn(`Sign verification failed, installer signed with incorrect certificate: ${result}`)
        resolve(result)
      } catch (e: any) {
        handleError(logger, e, null, reject)
        resolve(null)
        return
      }
    })
  })
}

function parseOut(out: string): any {
  const data = JSON.parse(out)
  delete data.PrivateKey
  delete data.IsOSBinary
  delete data.SignatureType
  const signerCertificate = data.SignerCertificate
  if (signerCertificate != null) {
    delete signerCertificate.Archived
    delete signerCertificate.Extensions
    delete signerCertificate.Handle
    delete signerCertificate.HasPrivateKey
    // duplicates data.SignerCertificate (contains RawData)
    delete signerCertificate.SubjectName
  }
  return data
}

function handleError(logger: Logger, error: Error | null, stderr: string | null, reject: (reason: any) => void): void {
  if (isOldWin6()) {
    logger.warn(
      `Cannot execute Get-AuthenticodeSignature: ${error || stderr}. Ignoring signature validation due to unsupported powershell version. Please upgrade to powershell 3 or higher.`
    )
    return
  }

  try {
    execFileSync(...preparePowerShellExec("ConvertTo-Json test", 10 * 1000))
  } catch (testError: any) {
    logger.warn(
      `Cannot execute ConvertTo-Json: ${testError.message}. Ignoring signature validation due to unsupported powershell version. Please upgrade to powershell 3 or higher.`
    )
    return
  }

  if (error != null) {
    reject(error)
  }

  if (stderr) {
    reject(new Error(`Cannot execute Get-AuthenticodeSignature, stderr: ${stderr}. Failing signature validation due to unknown stderr.`))
  }
}

function isOldWin6(): boolean {
  const winVersion = os.release()
  return winVersion.startsWith("6.") && !winVersion.startsWith("6.3")
}
